De computertaal Python werd ooorspronkelijk ontworpen
als een open source computertaal die voor iedereen makkelijk
te leren en te programmeren zou zijn.
Dat verklaart de enorme populariteit ervan.
Maar door die enorme populariteit is het in de loop der tijd
toch ook een heel complexe taal geworden.
Er zijn eindeloos veel boeken verschenen over Python.
De inhoud van deze boeken wil ik niet herhalen.
Deze pagina gaat ervanuit dat je met de eerste beginselen
van de taal al kennisgemaakt hebt.
Op deze webpagina verzamel ik allerlei weetjes
die ik makkelijk wil kunnen terugvinden.
User input
Met de functie input() kun je iemand iets laten intypen. Wat de gebruiker intikt wordt altijd als 'string' geïnterpreteerd. Dat betekent dat Python de ingetypte tekst ziet als een opeenvolging van lettertekens zonder enige betekenis. Als je dus 12 intikt, dan ziet Python dat niet per se als een getal, maar als een één gevolgd door een twee. Mocht je willen dat wat ingetikt wordt anders wordt opgevat, moet je het ingetikte converteren naar het gewenste gegevenstype.
def main(): x = input('x = ') print(type(x)) if x.isnumeric(): x = int(x) print(type(x)) if __name__ == '__main__': main()
Python kent verschillende soorten gegevens. De meest bekende zijn:
letters | characters | str() |
gehele getallen | integers | int() |
drijvende-komma-getallen | floating point numbers | float() |
complexe getallen | complex numbers | |
waar of niet waar | Booleans |
In Python wordt geen decimale komma gebruikt, maar een decimale punt.
Een bestand kun je met de volgende code aanmaken:
def main(): file_object = open('outfile.csv', 'w') file_object.write("Hello\n") file_object.close() if __name__ == '__main__': main()
Voor mijn privé-programma's, vind ik csv-bestanden inlezen de gemakkelijkste vorm van invoer. Csv-bestanden kun je makkelijk aanmaken met Kladblok of Excel. Mijn voorkeur gaat uit naar de puntkomma als scheidingsteken. Het inlezen van een bestand kan in Python op verschillende manieren. De eerste manier is door een bestand te openen voor lezen, vervolgens het bestand te doorlopen met een for-loop. Bij een csv-bestand kun je in die for-loop elk record splitsen in verschillende velden. Na verwerking moet je het bestand weer te sluiten.
def main(): file_object = open('myfile.csv', 'r') for line in file_object: print(line.strip('\n')) velden = line.split(';') for v in velden: print(v) file_object.close() if __name__ == '__main__': main()
Een andere manier om een bestand in te lezen gaat met behulp van een with-context. Daarbij hoef je de file niet te sluiten, want de context van het with-statement zorgt ervoor dat dat gebeurt :
import sys def main(): filename = 'MyFile.txt' try: with open(filename) as f_input: for line in f_input: line = line.strip('\n') print(line) except Exception as err: print('(1) err') print( err ) print('(2) sys.exc_info()[0]') print( sys.exc_info()[0] ) # exception class print('(3) sys.exc_info()[1]') print( sys.exc_info()[1] ) # value print('(4) sys.exc_info()[2]') print( sys.exc_info()[2] ) # traceback object print('=====') if __name__ == '__main__': main()
Als het bestand MyFile.txt niet bestaat, geeft het programma de volgende output:
(1) err [Errno 2] No such file or directory: 'MyFile.txt' (2) sys.exc_info()[0] <class 'FileNotFoundError'> (3) sys.exc_info()[1] [Errno 2] No such file or directory: 'MyFile.txt' (4) sys.exc_info()[2] <traceback object at 0x000002C6B50EB200> =====
Je kunt het afhandelen vvan de fout in bovenstaand programma wat eleganter laten verlopen door de de class die in bovenstaande foutmelding werd genoemd, afzonderlijk af te handelen:
import sys def main(): filename = 'MyFile.txt' try: with open(filename) as f_input: for line in f_input: line = line.strip('\n') print(line) except FileNotFoundError: print('Bestand ' + filename + ' is niet aanwezig') except Exception as err: print('(1) err') print( err ) print('(2) sys.exc_info()[0]') print( sys.exc_info()[0] ) # exception class print('(3) sys.exc_info()[1]') print( sys.exc_info()[1] ) # value print('(4) sys.exc_info()[2]') print( sys.exc_info()[2] ) # traceback object print('=====') if __name__ == '__main__': main()
Als je de html-file die gaat over CSS, als invoerbestand neemt, wordt het programma maar deels uitgevoerd. Het eindigt met de volgende regels:
Als de viewport breed genoeg is, staan de verschillende blokken naast elkaar. (1) err 'charmap' codec can't decode byte 0x9d in position 3699: character maps to(2) sys.exc_info()[0] (3) sys.exc_info()[1] 'charmap' codec can't decode byte 0x9d in position 3699: character maps to (4) sys.exc_info()[2] <traceback object at 0x000001FA93C32E80> =====
Deze informatie is niet voldoende om te weten wat er aan de hand is. We breiden de code uit:
import sys, traceback def main(): filename = 'MyFile.txt' try: with open(filename) as f_input: for line in f_input: line = line.strip('\n') print(line) except FileNotFoundError: print('Bestand ' + filename + ' is niet aanwezig') except Exception as err: print('(1) err') print( err ) print('(2) sys.exc_info()[0]') print( sys.exc_info()[0] ) # exception class print('(3) sys.exc_info()[1]') print( sys.exc_info()[1] ) # value print('(4) sys.exc_info()[2]') print( sys.exc_info()[2] ) # traceback object print('(5) err.object') print( err.object ) print('(6) traceback.print_exception(err)') traceback.print_exception(err) print('(7) traceback.print_tb(err)') traceback.print_tb(err) # traceback print('=====') if __name__ == '__main__': main()
De output wordt:
Als de viewport breed genoeg is, staan de verschillende blokken naast elkaar. (1) err 'charmap' codec can't decode byte 0x9d in position 3699: character maps to <undefined> (2) sys.exc_info()[0] <class 'UnicodeDecodeError'> (3) sys.exc_info()[1] 'charmap' codec can't decode byte 0x9d in position 3699: character maps to <undefined> (4) sys.exc_info()[2] <traceback object at 0x0000022A10433940> (5) err.object Squeezed text (64 lines). (6) traceback.print_exception(err) Traceback (most recent call last): File "E:\Site\Site20240516\prive\ict\extra\test0001.py", line 7, in main for line in f_input: File "C:\Users\Wim\AppData\Local\Programs\Python\Python312\Lib\encodings\cp1252.py", line 23, in decode return codecs.charmap_decode(input,self.errors,decoding_table)[0] ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UnicodeDecodeError: 'charmap' codec can't decode byte 0x9d in position 3699: character maps to <undefined> (7) traceback.print_tb(err) Traceback (most recent call last): File "E:\Site\Site20240516\prive\ict\extra\test0001.py", line 7, in main for line in f_input: File "C:\Users\Wim\AppData\Local\Programs\Python\Python312\Lib\encodings\cp1252.py", line 23, in decode return codecs.charmap_decode(input,self.errors,decoding_table)[0] UnicodeDecodeError: 'charmap' codec can't decode byte 0x9d in position 3699: character maps to <undefined> During handling of the above exception, another exception occurred: Traceback (most recent call last): File "E:\Site\Site20240516\prive\ict\extra\test0001.py", line 29, in <module> main() File "E:\Site\Site20240516\prive\ict\extra\test0001.py", line 26, in main traceback.print_tb(err) # traceback File "C:\Users\Wim\AppData\Local\Programs\Python\Python312\Lib\traceback.py", line 55, in print_tb print_list(extract_tb(tb, limit=limit), file=file) File "C:\Users\Wim\AppData\Local\Programs\Python\Python312\Lib\traceback.py", line 74, in extract_tb return StackSummary._extract_from_extended_frame_gen( File "C:\Users\Wim\AppData\Local\Programs\Python\Python312\Lib\traceback.py", line 418, in _extract_from_extended_frame_gen for f, (lineno, end_lineno, colno, end_colno) in frame_gen: File "C:\Users\Wim\AppData\Local\Programs\Python\Python312\Lib\traceback.py", line 355, in _walk_tb_with_full_positions positions = _get_code_position(tb.tb_frame.f_code, tb.tb_lasti) AttributeError: 'UnicodeDecodeError' object has no attribute 'tb_frame'
Deze foutmelding is te complex voor mij. Ik ga op zoek op Internet, en ontdek dat anderen al meer dan 6 jaar eerder met deze foutmelding worstelden. Als mogelijke oorzaak wordt geopperd dat het bestand niet met utf-8 is aangemaakt. Ik heb de input-file aangemaakt met behulp van het Kladblok-programma in Windows. Windows schijnt niet met utf-8 te werken. De suggesties die op stackoverklow.com worden gegeven, zijn:
De eerste suggestie leidt tot:
import sys, traceback def main(): filename = 'MyFile.txt' tel = 0 try: with open(filename, 'rb') as f_input: for b_line in f_input: line = b_line.decode('utf-8') line = line.strip('\n') print(line) except FileNotFoundError: print('Bestand ' + filename + ' is niet aanwezig') except Exception as err: print('(1) err') print( err ) print('(2) sys.exc_info()[0]') print( sys.exc_info()[0] ) # exception class print('(3) sys.exc_info()[1]') print( sys.exc_info()[1] ) # value print('(4) sys.exc_info()[2]') print( sys.exc_info()[2] ) # traceback object print('(5) err.object') print( err.object ) print('(6) traceback.print_exception(err)') traceback.print_exception(err) print('(7) traceback.print_tb(err)') traceback.print_tb(err) # traceback print('=====') if __name__ == '__main__': main()
Het volgende programma telt een aantal getallen bij elkaar op.
def main(): numbers = [1, 2, 3, 5, 7, 11, 13, 17, 19, 23, 29] quantity = len(numbers) total = 0 i = 0 while i < quantity: total = total + numbers[i] i = i + 1 print(quantity) print(total) if __name__ == '__main__': main()
➋ In het while-statement wordt de voorwaarde 'i < quantity' getest. Er zijn twee plaatsen die voorafgaand aan deze test kunnen worden uitgevoerd. Beide zorgen ervoor dat de voorwaarde het juiste resultaat oplevert. Dat zijn ➊ het statement 'i = 0' voorafgaand aan het while-statement en ➌ het statement 'i = i + 1' aan het einde van block dat wordt uitgevoerd als de voorwaarde True oplevert.
Ik vind het leuk om af en toe op een primitieve manier te programmeren. Met primitief bedoel ik dat enkel gebruik wordt gemaakt van if- en while-statements. Ik gebruik dus geen for-statements, en geen object-georiënteerde technieken. Dus geen classes. Een soort heimwee naar de taal Algol, de eerste computertaal waarin ik geprogrammeerd heb.
def lees_bestand(): file_name = 'een_csv_bestand.csv' file_object = open(file_name) line = file_object.readline() print(line) while line != '': line = file_object.readline() print(line) file_object.close() lees_bestand()
Als het input-bestand de volgende inhoud heeft
David;20240112;35.00 Jim;20240123;44.00 Ken;20240125;32.81 John;20240131;4.33 David;20240204;35.00 John;20240205;21.61 Ken;20240221;12.32 David;20240228;16.37
dan ziet de output er als volgt uit:
David;20240112;35.00 Jim;20240123;44.00 Ken;20240125;32.81 John;20240131;4.33 David;20240204;35.00 John;20240205;21.61 Ken;20240221;12.32 David;20240228;16.37
Ten opzichte van de input, worden er lege regels toegevoegd.
Dat komt omdat er in het input-bestand aan het einde van elke regel
een onzichtbaar nieuwe-regel-teken staat.
Als ik het bestand inlees als binary-bestand, met het
volgende programma
input_file = open('py0051.csv', 'rb') inhoud_bestand = input_file.read() input_file.close print(inhoud_bestand)
dan krijg ik op mijn Windows-computer als output
De onzichtbare tekens \r en \n worden nu getoond. De tekenreeks begint met b gevolgd door een enkele quote (') en eindigt met een enkele quote. Dat wil zeggen dat het om een binary-weergave gaat. We gaan achterhalen welke binaire code er verscholen zit achter de tekens \r en \n.
x = b'\r' print( ord(x) ) # 13 print( hex(ord(x)) ) # 0xd x = b'\n' print( ord(x) ) # 10 print( hex(ord(x)) ) # 0xa
Op internet kun je met de zoekterm 'ascii' vinden waar \r en \n voor staan:
deci- maal | hexa- deci- maal | symbool | engels | nederlands | |
\r | 13 | D | CR | carriage return | ga naar het begin van de regel |
\n | 10 | A | LF | line feed | ga naar de volgende regel |
Hexadecimaal betekent 16-tallig. Dat betekent dat je een getal niet met de cijfers 0 t/m 9 weergeeft, maar ook A, B, C, D, E en F als cijfers beschouwt, met A=10, B=11, C=12, D=13, E=14, F =15. Dus: het decimale getal 13 komt overeen met het hexadecimale getal D, en het decimale getal 10 komt overeen met het hexadecimale getal A.
def main(): x = 'a' print( ord(x) ) # 97 print( x.encode('utf-8') ) # b'a' print( bytes(x, 'utf-8') ) # b'a' print( hex(ord(x)) ) # 0x61 print( x.encode('utf-8').hex() ) # 61 print( bytes(x, 'utf-8').hex() ) # 61 n = 97 print( chr(n) ) # a b = b'\x61' print( str(b, 'utf-8') ) # a print( b.decode('utf-8') ) # a if __name__ == '__main__': main()
In Python kun je getallen gewoon optellen. Het programma
print(2 + 3) # 5
heeft als output gewoon 5.
Maar als je in plaats van getallen letters intikt, bijvoorbeeld
print(abc + def)
dan interpreteert Python abc en def als namen van vaiabelen of als verboden tekencombinaties. Als die variabelen in het voorgaande niet dedefinieerd zijn, geeft Python een foutmelding. In dit geval wordt een syntax-error gemeld, omdat def een tekencombinatie is die je niet mag gebruiken, omdat def al gebruikt wordt om functies en methoden te definiëren.
Wel kun je tekenreeksen abc en def 'optellen' als je aanhalingstekens om abc en def zet.
print('abc' + 'def') # 'abcdef'
Het resultaat van dat 'optellen' is dat Python de tekenreeksen achter elkaar zet. Dat betekent dat het optellen van gehele getallen iets anders is dan optellen van tekenreeksen.
Het programma
print( type(2) ) # <class 'int'> print( type(3) ) # <class 'int'> print( type(5) ) # <class 'int'> print( type('abc') ) # <class 'str'> print( type('def') ) # <class 'str'>
geeft aan dat 2, 3 en 5 van het type int zijn. int is een afkorting voor integer, dat is een geheel getal; str is de afkorting voor string, wat staat voor tekenreeks. Vandaar dat 2 + 3 een andere uitkomst geeft dan '2' + '3':
print(2 + 3) # 5 print('2' + '3') # '23'
Ook andere bewerkingen als aftrekken, delen en vermenigvuldigen, werken bij verschillende gegevenstypen net iets anders.
Als je gaat delen, bijvoorbeeld
print( 5 / 2 ) # 2.5 print( 6 / 2 ) # 3.0
dan zie je dat de uitkomst een getal is waarin een decimale punt voorkomt, ook als de uitkomst van de deling een geheel getal is. Als je het type van zo'n getal met decimale komma opvraagt,
print( type(2.5) ) # <class 'float'> print( type(3.0) ) # <class 'float'>
dan blijken 2.5 en 3.0 het type float te hebben, wat wil zeggen dat het drijvende-komma-getallen zijn. Drijvende-komma-getallen worden op een speciale manier opgeteld. Dat het een andere manier van optellen is als bij gewone gehele getallen, kun je zien in het volgende programma:
print( 1 + 1 + 1 - 3) / 10 ) # 0.0 print( 0.1 + 0.1 + 0.1 - 0.3) ) # 5.551115123125783e-17
e-17 staat voor 10-17. Beide optellingen zouden gelijk moeten zijn aan 0, maar door afrondingsverschillen is het antwoord bij het optellen van drijvende-komma-getallen bijna nul.
print( 1e-0 ) # 1.0 print( 1e-1 ) # 0.1 print( 1e-2 ) # 0.01 print( 1e-3 ) # 0.001
Het optellen en delen gaat bij drijvende-komma-getallen dus
op een andere manier dan bij gehele getallen.
Python kent ook decimale getallen.
import decimal print( decimal.Decimal(0.1) + decimal.Decimal(0.1) + decimal.Decimal(0.1) - decimal.Decimal(0.3) ) # Decimal(0.0)
print( type( decimal.Decimal(5.43) ) ) # <class 'decimal.Decimal'>
Bij decimale getallen kun je de precisie instellen.
import decimal print( decimal.Decimal(1) / decimal.Decimal(7) ) # 0.1428571428571428571428571429 decimal.getcontext().prec = 4 print( decimal.Decimal(1) / decimal.Decimal(7) ) # 0.1429
print( type( decimal.Decimal(5.43) ) ) # <class 'decimal.Decimal'>
print( 2j + 3j ) # 5j print( 2.0j + 3.0j ) # 5j print( 2.1j + 3.2j ) # 5.300000000000001j
In de wiskunde wordt het complexe getal (0, 1) meestal aangegeven met de letter i, maar in de elektrotechniek, waar i gebruikt wordt voor stroomsterkte, wordt meestal de letter j gebruikt. Python volgt de notatie die gebruikelijk is in de elektrotechniek.
print( type( 5j ) ) # <class 'complex'> print( type( 5.3j ) ) # <class 'complex'>
from fractions import Fraction x = Fraction(3, 5) y = Fraction(2, 5) print(x) # 3/5 print(y) # 2/5 print(x + y) # 1 print(x - y) # 1/5
print( type(x) ) # <class 'fractions.Fraction'> print( type(x + y) ) # <class 'fractions.Fraction'>
print( Fraction('0.25') ) # Fraction(1, 4) print( Fraction('0.25') + Fraction('1.25') ) # Fraction(3, 2)
Voor breuken bestaan verschillende conversie-functies.
from fractions import Fraction print( (3.5).as_integer_ratio() ) # 7, 2 print( Fraction( *(3.5).as_integer_ratio() ) ) # 7/2
print( Fraction.from_float(1.75) ) # Fraction(7, 4)
We hebben hierboven gezien dat je van elke variabele de class kunt opvragen met behulp van het statement type(). Naast de classes die standaard in Python aanwezig zijn, kun je ook zelf classes maken. Dat gaat als volgt:
class MyClass: pass obj = MyClass() print(MyClass) #print(obj) # <__main__.MyClass object at 0x0000024D8ED0A900> print(type(MyClass)) # print(type(obj)) #
Met het commando 'class MyClass' maak je een nieuwe class.
Met 'pass' geef je aan dat het een class betreft,
waar je dus nog bijna niets mee kunt doen.
Het enige wat je ermee kunt doen is een object maken.
Met 'obj = MyClass()' maak je een object met de naam obj van class MyClass.
Vervolgens blijken MyClass en obj te bestaan, want je kunt ze printen.
Vervolgens wordt met type gekeken wat de classes zijn van MyClass en obj.
MyClass blijkt van de class 'type' te zijn.
Er zijn al wel een hoop methoden aanwezig.
De methoden bij een object kun je opvragen met het commando dir().
class MyClass: pass obj = MyClass() print('***** Methoden van MyClass *****') print(dir(MyClass)) print('***** Methoden van obj *****') print(dir(obj))
De output is:
***** Methoden van MyClass *****
['__class__', '__delattr__', '__dict__', '__dir__',
'__doc__', '__eq__', '__firstlineno__', '__format__',
'__ge__', '__getattribute__', '__getstate__', '__gt__',
'__hash__', '__init__', '__init_subclass__', '__le__',
'__lt__', '__module__', '__ne__', '__new__', '__reduce__',
'__reduce_ex__', '__repr__', '__setattr__', '__sizeof__',
'__static_attributes__', '__str__', '__subclasshook__',
'__weakref__'] ***** Methoden van obj ***** ['__class__', '__delattr__', '__dict__', '__dir__',
'__doc__', '__eq__', '__firstlineno__', '__format__',
'__ge__', '__getattribute__', '__getstate__', '__gt__',
'__hash__', '__init__', '__init_subclass__', '__le__',
'__lt__', '__module__', '__ne__', '__new__', '__reduce__',
'__reduce_ex__', '__repr__', '__setattr__', '__sizeof__',
'__static_attributes__', '__str__', '__subclasshook__',
'__weakref__']
Er miljoenen drijvende-komma-getallen denkbaar. Anders gezegd: Op basis van een class (bijvoorbeeld drijvende-komma-getallen) kun je een veelheid aan verschillende objecten (miljoenen drijvende-komma-getallen) creëren. Voorbeeld:
a = 2.3 b = 6.53 c = 21.43 pi = 3.14159 print( type(a),type(b)), type(c)), type(pi))
De getoonde variabelen a, b,c en pi nooemen we instance-variables van de class float. genoemd. Soms wordt dat naar het Nederlands vertaald vertaald met instantie-variabelen. Een instantie-variabele kan bij elk object weer een andere waarde hebben. Als je zelf een class maakt met het class-commando, kun je zo'n instance-varable direct aan een object koppelen. In onderstaand voorbeeld wordt de instance-variable straal gekoppeld aan de objecten cirkel_1 en cirkel_2::
class Cirkel: pass cirkel_1 = Cirkel() cirkel_1.straal = 4.774653 cirkel_2 = Cirkel() cirkel_2.straal = 3.183102 print( 2 * 3.14159 * cirkel_1.straal) # 30.000004236539997 print( 2 * 3.14159 * cirkel_2.straal) # 20.00000282436 print( type( Cirkel ) ) # <class 'type'> print( type( cirkel_1 ) ) # <class '__main__.Cirkel'> print( type( cirkel_2 ) ) # <class '__main__.Cirkel'>
Wat je vaker tegenkomt, is dat instance_variables via een method __init__() in de class-definitie aan het object wordt gekoppeld. Ook een berekening kan in de class-definitie worden opgenomen. Zo'n berekening wordt als functie aan de class gekoppeld. Een functie die aan een class is gekoppeld, wordt een method genoemd.
class Cirkel: def __init__(self, straal): self.straal = straal def omtrek(self): return 2 * 3.14159 * self.straal cirkel_1 = Cirkel(4.774653) cirkel_2 = Cirkel(3.183102) print( cirkel_1.omtrek() ) # 30.000004236539997 print( cirkel_2.omtrek() ) # 20.00000282436
self verwijst naar het object. Hoewel __init__() twee variabelen kent, namelijk self en straal, hoef je in de commando's cirkel_1 = Cirkel(4.774653) en cirkel_2 = Cirkel(3.183102) bij class Cirkel maar één parameter (straal) mee te geven.
Een class-variabele is gekoppeld aan een class. Een class-variable is ook benaderbaar als er nog geen enkel object van de class is aangemaakt. Een class-variabele kan benaderd worden vauit alle objecten die op basis van die class zijn gemaakt.
import math class Cirkel: pi = math.pi def __init__(self, straal): self.straal = straal def omtrek(self): return 2 * Cirkel.pi * self.straal print( Cirkel.pi ) # 3.141592653589793 cirkel_1 = Cirkel(4.774653) cirkel_2 = Cirkel(3.183102) print( cirkel_1.pi ) # 3.141592653589793 print( cirkel_1.straal ) # 4.774653 print( cirkel_1.omtrek() ) # 30.00002957648093 print( cirkel_2.pi ) # 3.141592653589793 print( cirkel_2.straal ) # 3.183102 print( cirkel_2.omtrek() ) # 20.000019717653956
Om alle objecten van een class te doorlopen, Maak je eerst een lege list als class_variabele aan. Als je een nieuw object aanmaakt, voeg je in het daarbij behorende __init__()-commando een verwijzing naar dat nieuwe object toe aan die list. De list bevat dan uiteindelijk verwijzingen naar alle eerder aangemaakte objecten. In het volgende voorbeeld zijn de objecten cirkels, die alle behoren tot de class Cirkel. In de volgende code gebruiken we self.__class__ voor de class waartoe de toe te voegen cirkel behoort.
import math class Cirkel: pi = math.pi alle_cirkels = [] def __init__(self, straal): self.straal = straal self.__class__.alle_cirkels.append(self) print( Cirkel.alle_cirkels ) cirkel_1 = Cirkel(1) print( Cirkel.alle_cirkels ) cirkel_2 = Cirkel(2) print( Cirkel.alle_cirkels ) cirkel_3 = Cirkel(3) print( Cirkel.alle_cirkels )
Je kunt bijvoorbeeld de som van de oppervlakten van de cirkels berekenen. In onderstaand voorbeeld wordt gebruik gemaakt van @staticmethod. Dat is een decorator die aangeeft dat de method die erop volgt, totaal_oppervlakte(), weliswaar is ondergebracht bij de class Cirkel, maar eigenlijk niets met die class te maken heeft. Een static method is aanwezig in een class omdat het op een of andere manier logisch is om de method daar onder te brengen. Er is wel een zekere functionaliteit die te maken heeft met de class, maar er hoeven geen objecten van de class aanwezig te zijn om de method uit te voeren. Een static method heeft geen parameter self of cls. Een static method is een method die gekoppeld is aan de class, maar niet aan een enkel object van de class. Vanuit een static method kunnen de variabelen die gedefinieerd zijn in de class, niet gewijzigd worden.
import math class Cirkel: pi = math.pi alle_cirkels = [] def __init__(self, straal): self.straal = straal self.__class__.alle_cirkels.append(self) def oppervlakte(self): return self.__class__.pi * self.straal * self.straal @staticmethod def totaal_oppervlakte(): totaal = 0 for c in Cirkel.alle_cirkels: totaal += c.oppervlakte() return totaal cirkel_1 = Cirkel(1) cirkel_2 = Cirkel(2) cirkel_3 = Cirkel(3) print( Cirkel.totaal_oppervlakte() ) # 43.982297150257104
Je roept een static method normaliter uit vanuit de class, niet vanuit een object. In het voorbeeld hierboven: Cirkel.totaal_oppervlakte(), en niet: cirkel_1.totaal_oppervlakte().
Class-methods zijn methods die niet aan een object zijn gekoppeld, maar aan een class. Het verschil met static methods houdt in dat (1) class-methods wel class_variabelen kunnen wijzigen en (2) in een class-method als eerste parameter een verwijzing naar de class zelf is opgenomen. Deze parameter wordt doorgaans niet self genoemd, maar cls. Een class-method moet worden voorafgegaan door de decorator @classmethod.
import math class Cirkel: pi = math.pi alle_cirkels = [] def __init__(self, straal): self.straal = straal self.__class__.alle_cirkels.append(self) def oppervlakte(self): return self.__class__.pi * self.straal * self.straal @classmethod def totaal_oppervlakte(cls): totaal = 0 for c in Cirkel.alle_cirkels: totaal += c.oppervlakte() return totaal print( Cirkel.totaal_oppervlakte() ) cirkel_1 = Cirkel(1) cirkel_2 = Cirkel(2) cirkel_3 = Cirkel(3) print( Cirkel.totaal_oppervlakte() ) print('===') print( cirkel_1.oppervlakte() ) print( cirkel_1.totaal_oppervlakte() )
Om de oppervlakte van cirkel één te weten te komen, moet je method cirkel_1.oppervlakte() uitvoeren, en niet cirkel_1.totaal_oppervlakte(), omdat totaal_oppervlakte een class-method is en de oppervlakte van alle aanwezige cirkels berekent, niet alleen die van cikel_1.
Geometrische vormen, zoals een cirkel, een rechthoek
of een vierkant, hebben allemaal eeen oppervlakte.
Om die oppervlaktes te berekenen, bestaan er formules.
Oppervlakte kun je beschouwen als een eigenschap die
bij de vorm hoort, of als het resultaat van een
formule.
In onderstaand programma is de oppervlakte het
resultaat van een method:
class Rechthoek: def __init__(self, hoogte, breedte): self.hoogte = hoogte self.breedte = breedte def oppervlakte(self): return self.hoogte * self.breedte def main(): rechthoekje = Rechthoek(2, 4) print( rechthoekje.oppervlakte() ) # 8 if __name__ == '__main__': main()
We gaan nu een aantal wijzigingen doorvoeren. (1) Allereerst vervangen we self.hoogte door self._hoogte en self.breedte door self._breedte. We plaatsen dus een underscore voor de variabele_namen. Hiermee geef je aan dat het niet is toegestaan dat deze variabelen van buitenaf worden gewijzigd. Met 'van buitenaf' wordt dan bedoeld 'van buiten de class-definitie'. (2) Vervolgens plaatsen we een regel met de tekst '@property' direct voorafgaand aan de regel 'def oppervlakte(self):'. (3) Als laatste vervangen we de aanroep 'rechthoekje.oppervlakte()' door 'rechthoekje.oppervlakte', d.w.z. dat we in de oproep de tekens () weglaten
class Rechthoek: def __init__(self, hoogte, breedte): self._hoogte = hoogte self._breedte = breedte @property def oppervlakte(self): return self._hoogte * self._breedte def main(): rechthoekje = Rechthoek(2, 4) print( rechthoekje.oppervlakte ) # 8 if __name__ == '__main__': main()
Met de decorator @property kun je dus wat door een method wordt berekend opvatten als een eigenschap van een object, dat niet zomaar gewijzigd mag worden. Wijziging mag alleen plaatsvinden vanuit een method van de class zelf.
class Rechthoek: def __init__(self, hoogte, breedte): self._hoogte = hoogte self._breedte = breedte @property def oppervlakte(self): return self._hoogte * self._breedte def main(): rechthoekje = Rechthoek(2, 4) print( rechthoekje.oppervlakte ) # 8 rechthoekje.oppervlakte = 3 if __name__ == '__main__': main()
heeft als output
8
Traceback (most recent call last): File "xxx.py", line 17, inmain() File "xxx.py", line 14, in main rechthoekje.oppervlakte = 3 AttributeError: property 'oppervlakte' of 'Rechthoek' object has no setter
De conversie-formules voor graden Celsius en graden Fahrenheit zijn:
f = c * 9 / 5 + 32 c = (f - 32) * 5 / 9
We maken een class Temperatuur, waarin we de temperatuur opslaan in graden Celsius.
class Temperatuur: def __init__(self): self._celsius = 0 @property def celsius_naar_fahrenheit(self): return self._celsius * 9 / 5 + 32 def main(): temp = Temperatuur() print( temp._celsius , '\u00B0C') # 0 °C print( temp.celsius_naar_fahrenheit , '\u00B0F' ) # 32 °F if __name__ == '__main__': main()
Omdat we ook andere temperaturen dan 0 °C willen kunnen weergeven, voegen we een setter toe aan de class Temperatuur.
class Temperatuur: def __init__(self): self._celsius = 0 @property def celsius_naar_fahrenheit(self): return self._celsius * 9 / 5 + 32 @celsius_naar_fahrenheit.setter def celsius_naar_fahrenheit(self, nieuwe_celsius): return nieuwe_celsius * 9 / 5 + 32 def main(): temp = Temperatuur() print( temp._celsius , '\u00B0C') # 0 °C print( temp.celsius_naar_fahrenheit , '\u00B0F' ) # 32 °F temp._celsius = 100 print( temp._celsius , '\u00B0C') # 100 °C print( temp.celsius_naar_fahrenheit , '\u00B0F' ) # 212 °F if __name__ == '__main__': main()
Vanuit functie main(), die niet gedefineerd is binnen
de class Temperatuur, kunnen we toch de waarde van
_celsius in object temp wijzigen.
Als we de naam celsius_naar_fahrenheit wijzigen in
_fahrenheit, dan lijkt setter
_fahrenheit op een variabele, die meeverandert met
_celsius.
class Temperatuur: def __init__(self): self._celsius = 0 @property def _fahrenheit(self): return self._celsius * 9 / 5 + 32 @_fahrenheit.setter def _fahrenheit(self, nieuwe_celsius): return nieuwe_celsius * 9 / 5 + 32 def main(): temp = Temperatuur() print( temp._celsius , '\u00B0C') # 0 °C print( temp._fahrenheit , '\u00B0F' ) # 32 °F temp._celsius = 100 print( temp._celsius , '\u00B0C') # 100 °C print( temp._fahrenheit , '\u00B0F' ) # 212 °F if __name__ == '__main__': main()
Overigens, de regel 'return nieuwe_celsius * 9 / 5 + 32' mag je vervangen door 'self._fahrenheit = nieuwe_celsius * 9 / 5 + 32'.
Het 'Liskov substitutie principe' betekent dat, als je super-class
hebt waarvan een sub-class worrdt afgeleid,
de sub-class dezelfde interface en implementatie moet erven van de super-class
en dat objecten van de sub-class de objecten van de super-class kunnen
vervangen.
Anders gezegd:
Het 'Liskov substitutie principe' zegt dat een afgeleid object
- laten we dat Derived noemen -
die erft van class - welke we Base noemen -
het Base-object moet kunnen vervangen
zonder de gewenste eigenschappen van een programma te veranderen.
Een voorbeeld: Veronderstel dat je denkt dat een vierkant een
variant is van een rechthoek, en dat je de class Vierkant afleidt van
de class Rechthoek.
De eigenschap waar we dan onze aandacht op richten is oppervlakte,
die is ondergebracht in de Base-class Rechthoek.
In het programma hieronder ontstaat een fout, als je
gebruik gaat maken van de functie resize().
De functie resize() kan gebruikt worden voor een object van
de class Rechthoek(), maar niet voor een object van de class
Vierkant.
class Rechthoek: def __init__(self, hoogte, breedte): self._hoogte = hoogte self._breedte = breedte @property def oppervlakte(self): return self._hoogte * self._breedte def resize(self, nieuwe_hoogte, nieuwe_breedte): self._hoogte = nieuwe_hoogte self._breedte = nieuwe_breedte class Vierkant(Rechthoek): def __init__(self, zijde): super().__init__(zijde, zijde) def main(): rechthoekje = Rechthoek(2, 4) print( rechthoekje.oppervlakte ) # 8 vierkantje = Vierkant(2) print( vierkantje.oppervlakte ) # 4 rechthoekje.resize(3, 5) print( rechthoekje.oppervlakte ) # 15 vierkantje.resize(3, 5) print( vierkantje.oppervlakte ) # 15 ???? if __name__ == '__main__': main()
De oorzaak van het feit dat je resize() niet kunt gebruiken voor een object van class Vierkant, is dat Rechthoek twee parameters vereist (hoogte en breedte) en Vierkant maar één (zijde). Vierkant heeft dus een andere interface als Rechthoek. Hoewel je een vierkant als een rechthoek kunt opvatten, kun je de class Vierkant beter niet programmeren als een subclass van de class Rechthoek, omdat dat heel gemakkelijk tot fouten kan leiden.
Om iets zinnigs te kunnen doen met een class, kun je verschillende dingen doen. Eén daarvan is dat je een class kunt baseren op een andere class. Hieronder volgt een voorbeeld, waarin een eenheid aan de de class float wordt toegevoegd.
class FloatUnit(float): def __new__(cls, value, unit): instance = super().__new__(cls, value) instance.unit = unit instance.value = value return instance def __repr__(cls): return str(cls.value) + ' ' + str(cls.unit) afstand = FloatUnit(1000, 'km') print(afstand) # 1000 km print(type(afstand)) # <class '__main__.FloatUnit'>
De output van dit programma is:
1000 km <class '__main__.FloatUnit'>
Je definieert eerst een methode __new__() die bij de class FloatUnit hoort.
Deze wordt uitgevoerd onmiddellijk na het maken van de lege class.
De methode super().__new__() hoort bij de class float.
Deze methode zorgt ervoor, dat de class FloatUnit een attribuut met de naam instance krijgt,
die een blauwdruk is voor een een float-waarde.
In de daarop volgende commando's worden twee attributen aan deze blauwdruk toegevoegd,
namelijk een waarde (value) en een eenheid (unit).
In het return-commando wordt de attribuut instance gekoppeld aan de class FloatUnit.
In dit voorbeeld volgt het print-commando wat je in de methode __repr__() hebt gedefinieerd.
In het volgende voorbeeld wordt naast __repr__() ook de methode __str__() gedefinieerd.
Als __str__() gedefinieerd is, volgt print() de methode __str__() en niet de methode __repr__().
import decimal class AmountOfMoney(decimal.Decimal): def __new__(cls, value, unit, sign): instance = super().__new__(cls, value) instance.unit = unit instance.value = value instance.sign = sign return instance def __str__(cls): return str(cls.sign) + ' ' + str(cls.value) def __repr__(cls): return str( cls.value) + ' ' + str(cls.unit) geldbedrag = AmountOfMoney(1000, 'euro', '\N{EURO SIGN}') print(type(geldbedrag)) # <class '__main__.AmountOfMoney'> print(geldbedrag) # € 1000 print(geldbedrag.__str__()) # € 1000 print(geldbedrag.__repr__()) # 1000 euro
Aan een class kun je een method toevoegen met de naam __call__(). Als je een object van die class maakt, wordt de method __call__() niet uitgevoerd. Maar je kunt het object zelf uitvoeren. Iedere keer als je het object zelf aanroept op dezelfde manier als een method, wordt de methode __call__() uitgevoerd. Een class waarvoor een method __call__() is gedefinieerd, wordt een callable class genoemd. Voorbeeld:
class CallableClass: def __init__(self): self.counter = 0 def __call__(self): self.counter += 1 print( 'een callable class', self.counter ) def main(): c = CallableClass() print( c.counter ) # 0 print( callable(c) ) # True c() print( c.counter ) # 1 c() print( c.counter ) # 2 if __name__ == '__main__': main()
De output is:
0 True een callable class 1 1 een callable class 2 2
Iedere keer dat het object c wordt aangeroepen, wordt __call__(self) uitgeoverd, en wordt de teller die bij het object c hoort met 1 opgehoogd. Tussen de verschillende aanroepen door kun je bepaalde gegevens 'onthouden' en in een volgende aanroep opnieuw benaderen en gebruiken.
In elk kledingstuk dat je in een winkel koopt is een label ingenaaid. Daarop staat op hoeveel graden je het kledingstuk kunt wassen, of je het kledingsuk in bleekwater mag leggen, of je het kledingstuk in de wasdroger mag drogen, of je het kledingstuk mag strijken, en wellicht ook van welk materiaal het kledingstuk is gemaakt (katoen, wol, zijde, ...). Dat is allemaal praktische informatie, die je nodig hebt bij het gebruik van een kledingstuk.
Het feit dat in elk commercieel verhandeld kledingstuk zo'n label is ingenaaid,
doet vermoeden dat er regelgeving is, die voorschrijft dat zo'n label moet
worden ingenaaid. Hoe dat juridisch precies geregeld is, is bij de meeste mensen
niet bekend, maar om één of andere reden houden de kledingfabrikanten zich aan dit
voorschrift.
Een class is te vergelijken met zo'n voorschrift.
Een class beschrijft dus niet een concreet ding dat je kunt aanraken,
maar eerder een wetmatigheid, een regel waar iedereen zich aan houdt.
Een class beschrijft in de regel niet alle eigenschappen waaraan een object moet voldoen,
maar alleen die eigenschappen die voor het programma van belang zijn.
Zoals een kledinglabel relevante kenmerken van een kledingstuk beschrijft,
beschrijft een class de relevante eigeneschappen van een object.
Een object is te vergelijken met een commercieel verhandeld kledingstuk met een label erin,
met iets dat je kunt aanraken.
Een object is te vergelijken met een concreet ding,
dat aan de voorwaarden van het voorschrift voldoet.
Men spreekt over composition als twee classes met elkaar in verband staan,
maar verder totaal verschillend zijn.
Een voorbeeld is het verband tussen bibliotheken en boeken.
Bibliotheken zijn totaal andere dingen dan boeken.
Als je over een bibliotheek praat, denk je onmiddellijk aan verzameling boeken.
Dat duidt dus op een verband tussen de bibliotheken en boeken.
Dat verband kun je een naam geven, bijvoorbeeld 'is opgenomen in'.
Je kunt zeggen : een boek is opgenomen in een bibliotheek.
Bij composition komen objecten van een zekere class voor in een andere class.
class Boek: def __init__(self, titel, auteur, uitgeverij, isbn): self.titel = titel self.auteur = auteur self.uitgeverij = uitgeverij self.isbn = isbn def get_info(self): return f"{self.titel} / {self.auteur}" class Bibliotheek: def __init__(self, naam, boeken=None): self.naam = naam if boeken is None: self.boeken = [] else: self.boeken = boeken def show_boeken(self): print(f"Alle boeken in {self.naam}:") for boek in self.boeken: info = boek.get_info() print(f"- {info}") def main(): boek_1 = Boek('Woordenboek', 'Van Dale', 'Van Dale Lexicografie', '90-6648-427-6') boek_2 = Boek('Leerwoordenboek Nederlands', 'Marilene Gathier en Dorine de Kruyf', 'Coutinho', '978 90 6283 444 0') bibliotheek = Bibliotheek('Leuke boeken bibliotheek', [boek_1, boek_2]) bibliotheek.show_boeken() if __name__ == '__main__': main()
Broeken, truien, sokken, jassen, petten, schoenen, enzovoorts, zijn allemaal kledingstukken.
De vraag is: 'Wanneer noem je een voorwerp wel of niet een kledingstuk?'
Bijvoorbeeld: Beschouw je een bril als kledingstuk? Zijn lenzen te beshouwen als kledingstuk?
Beschouw je een oordopje als kledingstuk?
De voorwaarden die jou doen besluiten of je iets wel of niet als een kledingstuk wil zien,
die zullen van geval tot geval verschillen.
Soms praat je over een bril als kledingstuk, soms praat je over een bril als medisch hulpmiddel.
Bij inheritance gaat het over de voorwaarden die je in het programma gebruikt om te bepalen
of iets wel of niet tot een class behoort.
Als het programma kledingstukken betreft die je in een wasmachine kunt wassen,
dan behoren schoenen, brillen, lenzen en oordopjes binnen dat programma
duidelijk niet tot de kledingstukken, maar broeken, truien, sokken wel.
En bij jassen en petten hangt het misschien af van wat voor jassen en petten het zijn.
Bij inheritance gaat het om twee soorten classes.
Eén class is de bovenliggende class, die voor het algemene begrip staat,
en in ons voorbeeld met 'kledingstuk' correspondeert.
De andere classes zijn de onderliggende classes, die staan voor de verbijzonderingen, en
in ons voorbeeld met broeken, truien, sokken enz.
corresponderen.
In een inheritance-relatie geldt dat
Polyformisme is het verschijnsel dat sommige activiteiten dezelfde benaming hebben maar toch een heel anderssoortige actie inhouden. Je kunt het hebben over auto rijden, op de fiets rijden, paard rijden, in de trein rijden. Het wordt allemaal 'rijden' genoemd, maar als je het nader bekijkt, zijn het heel verschillende activiteiten.
Bij encapsulation gaat het erom om gegevens en methoden die bij elkaar behoren onder te brengen in één omgeving en ervoor te zorgen dat deze gegevens en methoden niet van buiten die omgeving gewijzigd of aangeroepen kunnen worden. Dergelijke gegevens en methoden worden private gegevens en private methoden genoemd. Zo'n omgeving is dan meestal een class. Voor het onderbrengen van gegevens en methoden in een bepaalde class wordt in het engels vaak het woord wrapping gebruikt, wat omhullen betekent. De gegevens die van van buiten de class niet gewijzigd mogen worden, laat je in Python beginnen met een underscore ( _ ). Als er situaties zijn waarin een gegeven toch van buiten de class gewijzigd moeten kunnen worden, maak je daar binnen de class een methode voor, die setter wordt genoemd. Een methode die speciaal bedoeld is om een gegeven te raadplegen is een getter.
Bij abstraction gaat het erom de ingewikkelde details van een programma voor de gebruiker verborgen te houden. Aan de gebruiker laat men enkel datgene zien, wat voor hem of haar van belang is. Denk aan het dashbord van een auto. Hoe de motor precies werkt, is voor de bestuurder niet van belang. Op het dashbord ziet de bestuurder alleen de gegevens die voor hem van belang zijn.
Nu volgen een aantal programmaatjes die laten zien hoe classes gebruikt kunnen worden.
In onderstaand programma wordt het attribuut n op twee manieren gewijzigd. Bij het maken van object a krijgt attribuut n de waarde 5, bij het uitvoeren van methode __init__(5). Via het statement 'a.n = 3' krijgt attribuut n de waarde 3 van buiten het object a. Via het statement 'a.ophogen_n()' krijgt attribuut n de waarde 4, maar nu vanuit een methode van object a zelf.
class A(): def __init__(self, n): self.n = n def ophogen_n(self): self.n = self.n + 1 def main(): a = A(5) print(a.n) # 5 a.n = 3 print(a.n) # 3 a.ophogen_n() print(a.n) # 4 main()
In het volgende voorbeeld bevat de class Bibliotheek de list lijst_boeken als attribuut. Deze list wordt met 3 boeken gevuld.
class Bibliotheek(): def __init__(self,): self.lijst_boeken = [] def voeg_boek_toe_aan_lijst(self, boek): self.lijst_boeken.append(boek) def print_boeken(self): for idx, item in enumerate(self.lijst_boeken): print(idx, item.titel) print( '---') class Boek(): def __init__(self, titel, auteur, uitgeverij, isbn): self.titel = titel self.auteur = auteur self.uitgeverij = uitgeverij self.isbn = isbn def main(): bieb = Bibliotheek() boek_1 = Boek('Jip en Janneke', 'Annie M.G. Schmidt', 'Querido', 'ISBN 978 90 451 0225 2') boek_2 = Boek('Pippi Langkous', 'Astrid Lindgren', 'Ploegsma', '978 90 216 8023 1') boek_3 = Boek('De zoete zusjes helpen de natuur', 'Hanneke de Zoete', 'Kosmos', '978 90 439 9384 2') bieb.print_boeken() bieb.lijst_boeken.append(boek_1) bieb.print_boeken() bieb.lijst_boeken.append(boek_2) bieb.print_boeken() bieb.lijst_boeken.append(boek_3) bieb.print_boeken() main()
De output is
--- 0 Jip en Janneke --- 0 Jip en Janneke 1 Pippi Langkous --- 0 Jip en Janneke 1 Pippi Langkous 2 De zoete zusjes helpen de natuur ---
Je wilt uit een attribuut van type list (self.collectie) het laatste item ophalen.
class Item(): def __init__(self, item): self.item = item class Collectie(): def __init__(self,): self.collectie = [Item('item_1'), Item('item_2'), Item('item_3')] def get_laatste_item(self): return self.collectie[-1].item def main(): collectie = Collectie() laatste_item = collectie.get_laatste_item() print( laatste_item ) # item_3 main()
In onderstaand voorbeeld halen via de class Company de naam van de eigenaar van een bedrijf op.
class Person: def __init__(self, name, age): self.name = name self.age = age class Company: def __init__(self, name, owner): self.name = name self.owner = owner def main(): john = Person('John', 22) bedrijf = Company('Bedrijf', john) print( bedrijf.owner.name, bedrijf.owner.age, sep = ' / ' ) # John / 22 main()
Via een keten van objecten kun je een methode die bij een andere class behoort, uitvoeren. Stel je voor dat A een motor voorstelt, en B een onderdeel van die motor. C stelt een onderdeel van B voor en D is weer een onderdeel van C. Je wilt vanuit class A een onderdeeltje D aansturen. In onderstaand voorbeeld maakt het uitvoeren van methode do_a() van een object van type A, dat onderdeeltje d van type D de actie do_d() uitvoert.
class A(): def do_a(self): b = B() b.do_b() class B(): def do_b(self): c = C() c.do_c() class C(): def do_c(self): d = D() d.do_d() class D(): def do_d(self): print( 'd.do_d' ) # d.do_d def main(): a = A() a.do_a() if __name__ == '__main__': main()
Standaard JSON kent (1) geen commentaar, (2) geen komma's waarna niets volgt en (3) geen enkele aanhalingstekens bij strings.
Het omzetten van gegevens naar het JSON-formaat wordt serialization genoemd.
Het tegenovergestelde proces, deserialization, houdt in dat gegevens in JSON-formaat wordt omgezet in een in Python-gegevenstype.
De volgende Python-code zet de python-gegevenstypen om in strings met json-code.
import json def python_to_json(): print(json.dumps({"naam": "Jan", "leeftijd": 31})) # dictionary -> json-object print(json.dumps(["appel", "banaan"])) # list -> json-array print(json.dumps(("appel", "banaan"))) # tuple -> json-array print(json.dumps("hallo")) # string -> json-string print(json.dumps(56)) # integer -> json-number print(json.dumps(41.87)) # float -> json-number print(json.dumps(True)) # True -> json-true print(json.dumps(False)) # False -> json-false print(json.dumps(None)) # None -> json-null python_to_json()
Het programma heeft als output:
{"naam": "Jan", "leeftijd": 31} ["appel", "banaan"] ["appel", "banaan"] "hallo" 56 41.87 true false null
De volgende Python-code zet json-gegevens om naar python-gegevenstypen .
import json def json_to_python(): print(json.loads('{"naam": "Jan", "leeftijd": 31}')) # json-object -> dictionary print(json.loads('["appel", "banaan"]')) # json-array -> list print(json.loads('"hallo"')) # json-string -> string print(json.loads('56')) # json-number -> integer print(json.loads('41.87')) # json-number -> float print(json.loads('true')) # json-true -> True print(json.loads('false')) # json-false -> False print(json.loads('null')) # json-null -> None json_to_python()
Het programma heeft als output:
{'naam': 'Jan', 'leeftijd': 31} ['appel', 'banaan'] hallo 56 41.87 True False None
Als input-file nemen we infile.json met de volgende inhoud:
[ {"naam": "Jan", "leeftijd": 31}, ["appel", "banaan"], "hallo", 56, 41.87, true, false, null ]
Het Python-programma om het json-bestand in te lezen, ziet er als volgt uit:
import json def read_json_file(): with open("infile.json", mode="r", encoding="utf-8") as jsonfile: infile_data = json.load(jsonfile) print(infile_data) read_json_file()
Als output krijgen we:
[{'naam': 'Jan', 'leeftijd': 31}, ['appel', 'banaan'], 'hallo', 56, 41.87, True, False, None]
import json def write_json_file(): python_list = [{'naam': 'Jan', 'leeftijd': 31}, ['appel', 'banaan'], 'hallo', 56, 41.87, True, False, None] with open("outfile.json", mode="w", encoding="utf-8") as jsonfile: json.dump(python_list, jsonfile) write_json_file()
De inhoud van het weggeschreven bestand is:
[{"naam": "Jan", "leeftijd": 31}, ["appel", "banaan"], "hallo", 56, 41.87, true, false, null]
import json def write_json_file(): python_dict = { 'naam': 'Jan', 'leeftijd': 31, 'woonplaats': 'Rotterdam' } with open("outfile.json", mode="w", encoding="utf-8") as jsonfile: json.dump(python_dict, jsonfile) write_json_file()
De inhoud van het weggeschreven bestand is:
{"naam": "Jan", "leeftijd": 31, "woonplaats": "Rotterdam"}
In dit voorbeeld werd een string als key gebruikt. Er zijn een drietal gegevenstypen die in Json niet als key mogen worden gebruikt. Dat zijn dict, list en tuple. Overzicht:
Python data type | Toegestaan als JSON key | |
dict | ✗ | |
list | ✗ | |
tuple | ✗ | |
str | ✓ | |
int | ✓ | |
float | ✓ | |
bool | ✓ | |
None | ✓ |
Als input-file nemen we rechthoeken.json met de volgende inhoud:
[ { "illustration_upper_left_x": 0, "illustration_upper_left_y": 0, "element_x": 10, "element_y": 5, "element_width": 300, "element_height": 30, "element_top": 10, "element_right": 10, "element_bottom": 10, "element_color": "lightGreen", "text_x": 15, "text_y": 24, "text_length": 100, "text": "the quick brown fox jumps over the lazy dog" }, { "illustration_upper_left_x": 0, "illustration_upper_left_y": 0, "element_x": 10, "element_y": 5, "element_width": 300, "element_height": 30, "element_top": 10, "element_right": 10, "element_bottom": 10, "element_color": "lightGreen", "text_x": 15, "text_y": 24, "text_length": 100, "text": "filmquiz bracht knappe ex-yogi van de wijs" }, { "illustration_upper_left_x": 0, "illustration_upper_left_y": 0, "element_x": 10, "element_y": 5, "element_width": 300, "element_height": 30, "element_top": 10, "element_right": 10, "element_bottom": 10, "element_color": "lightGreen", "text_x": 15, "text_y": 24, "text_length": 100, "text": { "text_1": "Als beginnend concertist debuteerde een ", "text_2": "fijngevoelige gitarist, hierna improviseerden", "text_3": "jeugdige klankkunstenaars levendig maar", "text_4": "notenblind op Peruviaanse quena's, robuuste", "text_5": "slagwerkers trommelden uitzinnige volksmuziek,", "text_6": "waarna xylofonisten 'Yesterday' zongen.; " } } ]
Het Python-programma om het json-bestand rechthoeken.json in te lezen, ziet er als volgt uit:
import json def read_json_file(): with open("rechthoeken.json", mode="r", encoding="utf-8") as jsonfile: infile_data = json.load(jsonfile) print(infile_data) read_json_file()
De outputfile bestaat uit één regel waarin de gehele list is opgenomen.
Als je binnen je programma een beter beeld wilt hebben van wat je hebt ingelezen, kun je de ingelezen data formatteren. Bijvoorbeeld:
import json def read_json_file(): with open("rechthoeken.json", mode="r", encoding="utf-8") as jsonfile: infile_data = json.load(jsonfile) formatted_data = json.dumps(infile_data, indent=4, separators=(". ", " = ")) print(formatted_data) read_json_file()
De output wordt dan als volgt geformatteerd:
[ { "illustration_upper_left_x" = 0. "illustration_upper_left_y" = 0. "element_x" = 10. "element_y" = 5. "element_width" = 300. "element_height" = 30. "element_top" = 10. "element_right" = 10. "element_bottom" = 10. "element_color" = "lightGreen". "text_x" = 15. "text_y" = 24. "text_length" = 100. "text" = "the quick brown fox jumps over the lazy dog" }. { "illustration_upper_left_x" = 0. "illustration_upper_left_y" = 0. "element_x" = 10. "element_y" = 5. "element_width" = 300. "element_height" = 30. "element_top" = 10. "element_right" = 10. "element_bottom" = 10. "element_color" = "lightGreen". "text_x" = 15. "text_y" = 24. "text_length" = 100. "text" = "filmquiz bracht knappe ex-yogi van de wijs" }. { "illustration_upper_left_x" = 0. "illustration_upper_left_y" = 0. "element_x" = 10. "element_y" = 5. "element_width" = 300. "element_height" = 30. "element_top" = 10. "element_right" = 10. "element_bottom" = 10. "element_color" = "lightGreen". "text_x" = 15. "text_y" = 24. "text_length" = 100. "text" = { "text_1" = "Als beginnend concertist debuteerde een ". "text_2" = "fijngevoelige gitarist, hierna improviseerden". "text_3" = "jeugdige klankkunstenaars levendig maar". "text_4" = "notenblind op Peruviaanse quena's, robuuste". "text_5" = "slagwerkers trommelden uitzinnige volksmuziek,". "text_6" = "waarna xylofonisten 'Yesterday' zongen.; " } } ]
In Python creëren we een dictionary, die we op twee manieren omzetten naar json-formaat. De tweede manier bevat minder spaties, en is daardoor korter.
import json def create_json(): # creëer dictionary json_dict json_dict = { 1: { "illustration_upper_left_x": 0, "illustration_upper_left_y": 0, "element_x": 10, "element_y": 5, "element_width": 300, "element_height": 30, "element_top": 10, "element_right": 10, "element_bottom": 10, "element_color": "lightGreen", "text_x": 15, "text_y": 24, "text_length": 100, "text": "the quick brown fox jumps over the lazy dog" }, 2: { "illustration_upper_left_x": 0, "illustration_upper_left_y": 0, "element_x": 10, "element_y": 5, "element_width": 300, "element_height": 30, "element_top": 10, "element_right": 10, "element_bottom": 10, "element_color": "lightGreen", "text_x": 15, "text_y": 24, "text_length": 100, "text": "filmquiz bracht knappe ex-yogi van de wijs" } } # zet dictionary json_dict om in string json_data json_data = json.dumps(json_dict) print('aantal bytes in json_data :', len(json_data)) # schrijf json_data weg als bestand outfile1.json with open("outfile1.json", mode="w", encoding="utf-8") as output_file: output_file.write(json_data) # zet dictionary json_dict om in string mini_json mini_json = json.dumps(json_dict, indent=None, separators=(",", ":")) print('aantal bytes in mini_json :', len(mini_json)) # schrijf mini_jason weg als bestand outfile2.json with open("outfile2.json", mode="w", encoding="utf-8") as output_file: output_file.write(mini_json) create_json()
De output is:.
De volgende python-code zet het csv-bestand 'infile.csv' om naar het json-bestand 'outfile.json':
import csv import json def csv_to_json(csvFilePath, jsonFilePath): jsonArray = [] try: with open(csvFilePath, encoding='utf-8') as csvf: csvReader = csv.DictReader(csvf) for row in csvReader: jsonArray.append(row) with open(jsonFilePath, 'w', encoding='utf-8') as jsonf: jsonString = json.dumps(jsonArray, indent=4) jsonf.write(jsonString) except FileNotFoundError: print("csv_to_json: csv-input-file niet gevonden " + csvFilePath) def main(): csvFilePath = r'infile.csv' jsonFilePath = r'outfile.json' csv_to_json(csvFilePath, jsonFilePath) if __name__ == '__main__': main()
Een gewone manier om een variabele te definiëren is :
a = 15 print(a)
De output is:
Je kunt specificeren welk type de variabele moet hebben :
a: int = 15 print(a)
De output blijft:
Intern in Python wordt in de directory __annotations__ de type-hint opgeslagen:
a: int = 15 print(a) print(__annotations__)
De output wordt:
De python-interpreter doet helemaal niets met de toevoeging van ': int'. Het is meer bedoeld als herinnering voor de programmeur, in de trant van 'het is de bedoeling dat een variabele met de naam a van het type int is. ' Als een programmeur zich niet aan zo'n voornemen houdt, genereert de python-interpreter geen errors of warnings. Het volgende programma kent de waarde 15.72 van het type float toe aan variabele a, maar specificeert dat a een integer-waarde zou moeten hebben. Niettemin wordt er bij uitvoeriing van het programma geen waarschuwing gegenereerd:
a: int = 15.72 print(a) print(__annotations__)
De output is:
Toevoegingen als ': int' of ': float' worden 'type hints' genoemd Sommige editors (PyCharm) en programma's van externe partijen (MyPy) gebruiken type hints om inconsistenties in programma's op te sporen.
s: str = "abcd" i: int = 15 f: float = 41.25 l: list = [ "a", "b" ] t: tuple = ( "a", "b" ) d: dict = { 1 : "a" } b: bool = True n: None = None print(s) print(i) print(f) print(l) print(t) print(d) print(b) print(n) print('-----') print(__annotations__)
De output is:
In het volgende programma worden drie functies zonder type hints gedefinieerd:
def func_1(var): return print(var) def func_2(var = "x"): return print(var) def func_3(var = "y"): print(var) return var func_1("a") func_2() func_2("b") func_3() func_3("c")
Bij func_2 is de default-waarde voor variabele var gelijkgesteld aan "x". Bij func_3 is deze default-waarde gelijkgesteld aan "y". De output is:
Je kunt aan deze functies type-hints toevoegen De type-hint voor de return-waarde van de functie geeft je aan met '->':
def func_1(var: str): return print(var) def func_2(var: str = "x"): return print(var) def func_3(var: str = "y") -> str: print(var) return var func_1("a") func_2() func_2("b") func_3() func_3("c") print('-----') print(__annotations__) print(func_1.__annotations__) print(func_2.__annotations__) print(func_3.__annotations__)
De output wordt
De volgende code definieert een list, een tuple en een dictionary:
namen = ["Jan", "Piet", "Kees"] versies = (3, 7, 2) opties = {"gecentreerd": False, "met_hoofdletters": True} print(namen[0], namen[1], namen[2]) print(versies[0], versies[1], versies[2]) print(opties["gecentreerd"], opties["met_hoofdletters"])
De output is:
Je kunt aan deze code type hints voor de individuele objecten in de list toevoegen. Je moet daarvoor wel delen van de standaard-module typing importeren.
from typing import Dict, List, Tuple namen: List[str] = ["Jan", "Piet", "Kees"] versies: Tuple[int, int, int] = (3, 7, 1) opties: Dict[str, bool] = {"gecentreerd": False, "met_hoofdletters": True} print(namen[0], namen[1], namen[2]) print(versies[0], versies[1], versies[2]) print(opties["gecentreerd"], opties["met_hoofdletters"]) print('-----') print(__annotations__)
De output wordt:
We gaan uit van een programma waarin een class en een object worden gedefinieerd.
import math class Mal(): pi = math.pi def __init__(self, radius): self.radius = radius def omtrek_en_oppervlakte(self): self.omtrek = 2 * self.pi * self.radius self.oppervlakte = self.pi * self.radius * self.radius return self.omtrek, self.oppervlakte def main(): object_1 = Mal(10) (omtrek, oppervlakte) = object_1.omtrek_en_oppervlakte() print(omtrek, oppervlakte) main()
De output wordt:
Aan het programma voegen we type-hints toe.
import math class Circle_template(): print('----- Circle_template() ----- class definiton -----') pi: float = math.pi print('Circle_template: __annotations__ =', __annotations__) def __init__(self, radius): print('-----__init__(self, radius) ----- method') self.radius: float = radius print('__init__(self, radius): self.__annotations__ ==', self.__annotations__) def omtrek_en_oppervlakte(self): print('----- omtrek_en_oppervlakte(self) ----- method') self.omtrek: float = 2 * self.pi * self.radius self.oppervlakte: float = self.pi * self.radius * self.radius print('omtrek_en_oppervlakte(self): __annotations__ =', __annotations__) print('omtrek_en_oppervlakte(self): self.__annotations__ ==', self.__annotations__) return self.omtrek, self.oppervlakte def main(): print('----- main() ----- function') circle = Circle_template(10) print( 'main(): circle.__annotations__ ==', circle.__annotations__) (omtrek, oppervlakte) = circle.omtrek_en_oppervlakte() print(omtrek, oppervlakte) print('----- program -----') main()
De output wordt:
Wat opvalt is dat omtrek en oppervlakte niet worden opgenomen in de annotations. Dit schijnt te maken te hebben met het feit dat een class een soort mal is, een template, dat als het gedefinieerd wordt, nog geen object is. Dit is voor mij vooralsnog onbekend terrein en ik ga er verder niet op in.
Refactoring gaat over het herschrijven van bestaande programmatuur. Het gaat erom om onduidelijke of onnodig gecompliceerde code om te zetten in goede code, zó, dat het programma precies blijft doen wat het altijd al deed. Waarom zou je bestaande programma's willen herschrijven? Daarvoor zijn verschillende redenen te bedenken:
In het volgende programma voldoet de functie containsEven() aan de regel, dat je maar vijf statements in een functie mag hebben, maar de functie minimum() niet. De functie containsEven(arr) checkt of de twee-dimensionale array arr een even getal bevat. De functie minimum(arr) bepaalt het laagste getal in de twee-dimensionale array arr.
def containsEven(arr): for x in arr: for y in x: if y % 2 == 0: return True return False def minimum(arr): r = (9**9)**9 for x in arr: for y in x: if y < r: r = y return r def main(): arr = [ [21, 23, 67], [35, 67, 33], [41, 58, 89], [23, 11, 73], ] r = containsEven(arr) print(r) r = minimum(arr) print(r) main()
De output wordt:
Als je er niet op let worden functies of methoden al maar langer. Daarmee wordt het lastiger ze te begrijpen.
Een hulpmiddel om te komen tot "five lines" is het refactoring-patroon "extract method. Deze passen we toe op bovenstaande functie minimum().
def minimum(arr): r = (9**9)**9 for x in arr: for y in x: #-------- if y < r: r = y #-------- return r def main(): arr = [ [21, 23, 67], [35, 67, 33], [41, 58, 89], [23, 11, 73], ] r = minimum(arr) print(r) main()
def minimum(arr): r = (9**9)**9 for x in arr: for y in x: #-------- #-------- return r #-------- def min(): if y < r: r = y #-------- def main(): arr = [ [21, 23, 67], [35, 67, 33], [41, 58, 89], [23, 11, 73], ] r = minimum(arr) print(r) main()
def minimum(arr): r = (9**9)**9 for x in arr: for y in x: #-------- r = min(r, arr, x, y) #-------- return r #-------- def min(r, arr, x, y): if y < r: r = y return r #-------- def main(): arr = [ [21, 23, 67], [35, 67, 33], [41, 58, 89], [23, 11, 73], ] r = minimum(arr) print(r) main()
def minimum(arr): r = (9**9)**9 for x in arr: for y in x: r = min(r, arr, x, y) return r def min(r, arr, x, y): if y < r: r = y return r def main(): arr = [ [21, 23, 67], [35, 67, 33], [41, 58, 89], [23, 11, 73], ] r = minimum(arr) print(r) main()
Een korte vertaling van de regel "either call or pass' zou kunnen zijn: "ofwel aanroepen, ofwel doorgeven, maar niet beide". Een functie zou dus ofwel methoden behorende bij een object moeten aanroepen, ofwel het object doorgeven als een argument, maar niet beide. Als je het voorbeeld dat daarbij in het boek "Five lines of code" wordt gegeven, vertaalt naar Python, krijg je zoiets als
def average(arr): return sum(arr) / arr.__len__() def main(): arr = [21, 23, 67, 35, 67, 33, 41, 58, 89, 23, 11, 84] print(average(arr)) main()
In sum(arr) wordt het object arr als argument doorgegeven aan de functie sum(), maar in arr.__len__() wordt een method van het object aangeroepen. In Python is dat nogal een gekunsteld voorbeeld, want arr.__len__() is een heel ongewone schrijfwijze van len(arr). Volgens de regel "either pass or call" zou je dit programma moeten verbeteren tot
def average(arr): return sum(arr) / len(arr) def main(): arr = [21, 23, 67, 35, 67, 33, 41, 58, 89, 23, 11, 84] print(average(arr)) main()
De reden van de regel "either call or pass" wordt uitgelegd als: Elk statement in een functie zou hetzelfde abstractie-niveau moeten hebben. Het doorgeven van een object als argument leidt in de regel tot een hoger abstractie-niveau dan het aanroepen van een method van een object. Vandaar dat je binnen een functie moet kiezen voor het één of het ander.
De verschillende vertakkingen binnen een if-statement moet je zo veel mogelijk door afzonderlijke functie/methoden laten afhandelen. In de volgende functie zie je drie if-statements staan.
import math as m def report_primes(n): for i in range(2, n): if is_prime(i): print(i) def is_prime(i): s = m.floor(m.sqrt(i)) + 1 has_factor = False for f in range(2, s): if i % f == 0: has_factor = True if has_factor == False: return True report_primes(100)
Als je het eerste if_statement vervangt door een aparte functie, krijg je
import math as m def report_primes(n): for i in range(2, n): report_prime(i) def report_prime(i): if is_prime(i): print(i) def is_prime(i): s = m.floor(m.sqrt(i)) + 1 has_no_factor = True for f in range(2, s): if i % f == 0: has_no_factor = False return has_no_factor report_primes(100)
Als ik de andere if-statements in een aparte functie probeer onder te brengen, vind ik het programma er niet duidelijker op worden.
De regel luidt: Gebruik geen else in een if-statement, behalve wanneer we te maken hebben met een variabele waarvan we de inhoud niet kunnen bepalen (bijvoorbeeld het indrukken van toets). De reden om else zo min mogelijk te gebruiken is omdat het verwarrend kan zijn onder welke omstandigheden tot de else-vertakking wordt overgegaan. In onderstaand programma
def gemiddelde(getallen_reeks): if len(getallen_reeks) == 0: print('een lege getallenreeks is niet toegestaan') else: return sum(getallen_reeks) / len(getallen_reeks) def main(): print(gemiddelde([1, 2, 3])) print(gemiddelde([100, 200, 300])) print(gemiddelde([])) if __name__ == '__main__': main()
vervangen we de vertakkingen van de if- en else-clausules door opeenvolgende functie-aanroepen. Elke van die functies begint met een if-statement zonder else-clausule.
def gemiddelde(getallen_reeks): check_getallen_reeks_leeg(getallen_reeks) return bereken_gemiddelde(getallen_reeks) def check_getallen_reeks_leeg(getallen_reeks): if len(getallen_reeks) == 0: print('een lege getallenreeks is niet toegestaan') return None def bereken_gemiddelde(getallen_reeks): if len(getallen_reeks) != 0: return sum(getallen_reeks) / len(getallen_reeks) def main(): print(gemiddelde([1, 2, 3])) print(gemiddelde([100, 200, 300])) print(gemiddelde([])) if __name__ == '__main__': main()
De output van beide programma's is hetzelfde:
2.0 200.0 een lege getallenreeks is niet toegestaan None
Als je deze werkwijze hanteert, wordt de programmatuur er dan duidelijker op? Als je niet in de gaten hebt, dat de functies check_getallen_reeks_leeg() en bereken_gemiddelde() beide met een if-statement beginnen, en de voorwaarden die bij die if-statements horen samen alle mogelijkheden vertegenwoordigen ( x == 0 en x != 0 vertegenwoordigen samen alle mogelijke waarden die x kan aannemen ), dan lijkt de inhoud van de functie gemiddelde() op een sequentie van een aantal acties.
Het boek 'Five lines of code' nodigt uit om onderscheid te maken tussen checks en decisions. Een if_statement is een check als er enkel gekeken wordt of er een bepaald iets aan de hand is. Een if-elif-statement kun soms meer opvatten als decision, als een beslissing die genomen moet worden. De filosofie is dat je beslissingen zo lang mogelijk moet uitstellen, dus zo laat mogelijk moet nemen. Om elif-clausules te vermijden propageert het boek 'Five lines of code' het refactoring-pattern 'Replace code with classes'.
Als je in een if-elif-constructie te maken hebt met verschillende categorieën, bijvoorbeeld met rood, oranje en groen (bij stoplichten) of small, medium en large (bij kledingmaten), breng dan elk van die categorieën onder in een eigen class. Als voorbeeld nemen we het volgende programma:
def comment_bank(bank): if bank.upper() == 'ASN': print('ASN') print('duurzaam en geen wapenindustrie') elif bank.upper() == 'TRIODOS': print('TRIODOS') print('duurzaam') elif bank.upper() == 'ING': print('ING') print('voert oranje leeuw als mascotte') elif bank.upper() == 'RABO': print('RABO') print('was ooit coöperatief') elif bank.upper() == 'ABN': print('ABN') print('werd ooit opgekocht door de overheid') def main(): bank = input('Bank ') comment_bank(bank) if __name__ == '__main__': main()
De gebruiker wordt gevraagd de naam van een bank in te voeren. Vervolgens wordt wat informatie over de bank getoond. De functie comment_bank() bevat een uitgebreid if-elif-else-statement. Dit programma gaan we in een aantal stappen herschrijven. Als een gebruiker ASN, Asn, asn of iets dergelijks intikt, verschijnt de tekst
ASN duurzaam en geen wapenindustrie
class Asn_bank(): name = 'ASN' def process_bank(self): print('ASN') print('duurzaam en geen wapenindustrie')
Iets soortgelijks doen we voor de andere banken die een gebruiker kan kiezen. We zorgen ervoor dat in de nieuwe classes het attribuut en de methode dezelfde naam hebben. We krijgen dan:
class Asn_bank(): name = 'ASN' def process_bank(self): print('ASN') print('duurzaam en geen wapenindustrie') class Triodos_bank(): name = 'TRIODOS' def process_bank(self): print('TRIODOS') print('duurzaam') class Ing_bank(): name = 'ING' def process_bank(self): print('ING') print('voert oranje leeuw als mascotte') class Rabo_bank(): name = 'RABO' def process_bank(self): print('RABO') print('was ooit coöperatief') class Abn_bank(): name = 'ABN' def process_bank(self): print('ABN') print('werd ooit opgekocht door de overheid')
Elke class heeft een attribuut name en een methode process_bank(). Het programma begint met het maken van een object voor elke class.
asn = Asn_bank() triodos = Triodos_bank() ing = Ing_bank() rabo = Rabo_bank() abn = Abn_bank()
We nemen deze objecten op in een list.
bank_list = [asn, triodos, ing, rabo, abn]
Dit doen we, omdat we daarmee de verschillende classes één voor één kunnen benaderen. We vragen aan een gebruiker een banknaam in te tikken.
bank_input = input('Bank ')
Daarna doorlopen we de lijst bank-objecten. Als we bij de bank zijn aangekomen, die de gebruiker heeft gespecificeerd, voeren we de methode process_bank() uit.
Het programma ziet er in zijn geheel dan als volgt uit:
class Asn_bank(): name = 'ASN' def process_bank(self): print('ASN') print('duurzaam en geen wapenindustrie') class Triodos_bank(): name = 'TRIODOS' def process_bank(self): print('TRIODOS') print('duurzaam') class Ing_bank(): name = 'ING' def process_bank(self): print('ING') print('voert oranje leeuw als mascotte') class Rabo_bank(): name = 'RABO' def process_bank(self): print('RABO') print('was ooit coöperatief') class Abn_bank(): name = 'ABN' def process_bank(self): print('ABN') print('werd ooit opgekocht door de overheid') def main(): # Maak bank-instellingen asn = Asn_bank() triodos = Triodos_bank() ing = Ing_bank() rabo = Rabo_bank() abn = Abn_bank() # Maak een lijst met banken bank_list = [asn, triodos, ing, rabo, abn] # Tik een bank in bank_input = input('Bank ') for bank in bank_list: if bank_input.strip().upper() == bank.name.upper(): bank.process_bank() if __name__ == '__main__': main()
We hebben nu het programma zodanig herschreven dat
het if-elif-statement vervangen is door een if-statement
zonder elif-clausules.
Als we gebruik maken van een dictionary in plaats van een list,
kan het programma zonder if-statement worden geschreven.
class Asn_bank(): def process_bank(self): print('ASN') print('duurzaam en geen wapenindustrie') class Triodos_bank(): def process_bank(self): print('TRIODOS') print('duurzaam') class Ing_bank(): def process_bank(self): print('ING') print('voert oranje leeuw als mascotte') class Rabo_bank(): def process_bank(self): print('RABO') print('was ooit coöperatief') class Abn_bank(): def process_bank(self): print('ABN') print('werd ooit opgekocht door de overheid') def main(): # Maak bank-instellingen asn = Asn_bank() triodos = Triodos_bank() ing = Ing_bank() rabo = Rabo_bank() abn = Abn_bank() # Maak dictionary met bank-instellingen bank_dict = {'ASN': asn, 'TRIODOS': triodos, 'ING': ing, 'RABO': rabo, 'ABN': abn } bank_input = input('bank ') try: bank_dict[bank_input.upper()].process_bank() except: pass if __name__ == '__main__': main()
Als voorbeeld nemen we het volgende programma:
def handle_event(invoer): print('handle_input(' + invoer + ')') if invoer == 'a': print('naar links') move_horizontal(-1) elif invoer == 'd': print('naar rechts') move_horizontal(1) elif invoer == 'w': print('omhoog') move_vertical(-1) elif invoer == 's': print('omlaag') move_vertical(1) def move_horizontal(x): print('move horizontal ' + str(x)) def move_vertical(y): print('move vertical ' + str(y)) def main(): invoer = input('a=links, d=rechts, w=omhoog, s=omlaag ') handle_event(invoer) if __name__ == '__main__': main()
Als een gebruiker een van de toetsen a, w, d of s indrukt, gaat er iets op het scherm naar links, naar boven , naar rechts of naar beneden. Het op en neer of omhoog en omlaag gaan wordt verzorgd door de functies move_vertical() en move_horizontal(). We willen in dit programma de functies move_horizontal() en move_vertical() dichterbij elkaar brengen. Daartoe creëren we voor elk van de vier mogelijkheden 'naar links', 'naar rechts, 'naar boven' en 'naar beneden' een class.
class Left(Invoer): def handle(): move_horizontal(-1) class Right(Invoer): def handle(): move_horizontal(1) class Up(Invoer): def handle(): move_vertical(-1) class Down(Invoer): def handle(): move_vertical(1)
Deze vier classes hebben dezelfde structuur. Merk op dat we bij bij de method handle() geen parameter self meegeven. Dat betekent dat het een class-method is. We hoeven geen gebruik te maken van objecten die zijn gebaseerd op Left, Right, Up en Down. De oorspronkelijke code herschrijven we nu.
class Left(): def handle(): move_horizontal(-1) class Right(): def handle(): move_horizontal(1) class Up(): def handle(): move_vertical(-1) class Down(): def handle(): move_vertical(1) def move_horizontal(x): print('move_horizontal(' + str(x) + ')') def move_vertical(y): print('move_vertical(' + str(y) + ')') def main(): prompt = 'a=links, d=rechts, w=omhoog, s=omlaag ' invoer = input(prompt) if invoer == 'a': invoer_class = Left elif invoer == 'd': invoer_class = Right elif invoer == 'w': invoer_class = Up elif invoer == 's': invoer_class = Down invoer_class.handle() if __name__ == '__main__': main()
Als een gebruiker een keuze heeft gemaakt, wordt afhankelijk wat hij heeft ingetikt, in de variabele invoer_class een verwijzing gemaakt naar één van de classes Left, Right, Up or Down. Omdat in elk van die classes de method handle() een andere inhoud heeft, wordt steeds de bijbehorende move-actie uitgevoerd.
In het volgende programma lijkt de functie rekening_bijwerken() overbodig.
class database: def update_rekening(rekening, bedrag): print('update_rekening :', bedrag, '->', rekening) def rekening_bijwerken(rekening, bedrag): database.update_rekening(rekening, bedrag) def overboeken(vanaf_rekening, naar_rekening, bedrag): rekening_bijwerken(vanaf_rekening, -bedrag) rekening_bijwerken(naar_rekening, bedrag) def main(): overboeken('ABN123', 'ING456', 543.21) if __name__ == '__main__': main()
Je kunt de code van de functie rekening_bijwerken() direct opnemen in de functie overboeken().
class database: def update_rekening(rekening, bedrag): print('update_rekening :', bedrag, '->', rekening) def overboeken(vanaf_rekening, naar_rekening, bedrag): database.update_rekening(vanaf_rekening, -bedrag) database.update_rekening(naar_rekening, bedrag) def main(): overboeken('ABN123', 'ING456', 543.21) if __name__ == '__main__': main()
Soms wordt de programmatuur begrijpelijker als een ingewikkelde functie wordt opgesplitst in functies die elk wat eenvoudiger te begrijpen zijn. In een spel kan een functie die antwoord geeft op de vraag 'welke vervolgacties zijn mogelijk?' soms beter gesplitst worden in bijvoorbeeld 'welke vervolgacties zijn mogelijk op de linkerflank?' en 'welke vervolgacties zijn mogelijk op de rechterflank?'.
Het woordenboek geeft als vertaling voor interface de woorden
grensvlak, raakvlak en koppeling.
In deze paragraaf gaat het over interfaces zoals die voorkomen
in de programmeertaal Java.
Interfaces in Java komen overeen met classes in Python,
die als blauwdruk fungeren voor andere classes.
Ze geven aan welke methods een class moet hebben,
maar niet wat die methods moeten doen.
Vertaald naar Python bevatten de methods van een interface
enkel het statement pass.
Volgens de regel 'Gebruik overerving alleen bij interfaces'
moet je alleen overerving gebruiken als de superclass
enkel de namen van de te definiëren methoden doorgeeft.
Wat die methoden doen, moet je in de class zelf opgeven.
Overerving wordt vaak gebruikt om een default implementatie
van een method te krijgen.
De nadelen daarvan zijn vaak veel ingrijpender dan de voordelen.
Code die door verschillende subklassen wordt gebruikt,
veroorzaakt koppelingen.
Vrij vertaald betekent dit: Gebruik overerving zo min mogelijk;
geef de vookeur aan composition.
Er zijn een aantal pakketten waarmee je functies en methodes die nergens worden aangeroepen in een Python-programma, kunt opsporen. Op internet vond ik (op 11-11-2024):
Ik kan mij goed voorstellen dat er situaties zijn, dat een programma begrijpelijker en beter onderhoudbaar is, wanneer wordt onderkend dat een aantal classes beter samengevoegd kunnen worden tot één class. Maar het is mij niet gelukt om in Python van een werkend programma te verzinnen, dat eenvoudig en kort genoeg is, en niet gekunsteld, om als voorbeeld te dienen.
Het volgende code-fragment bevat twee vertakkingen waarop de code 'x = 0'volgt.
if a > 0: x = 0 if a == 0: x = 1 if a < 0: x = 0
Deze code kun je herschrijven tot
if a == 0: x = 1 else x = 0
of
x = 0 if a == 0: x = 1
Welke codering je voorkeur heeft is een beetje een kwestie van smaak. De volgende oplossing die 'or' gebruikt wordt aanbevolen door de schrijver van het boek 'Five lines of code' omdat in alle vertakkingen van het if-statement een andere code staat en uit de code direct is af te lezen in welke gevallen welke vertakkingen worden aangeroepen..
if a < 0 or a > 0: x = 0 if a == 0: x = 1
Het is van belang de if-statements zo begrijpelijk mogelijk te houden. Soms is het prettig een voorwaarde in een aparte functie te evalueren.
def check_perioden_overlappen_elkaar( periode_1_vanaf, periode_1_tm, periode_2_vanaf, periode_2_tm): if periode_2_tm < periode_1_vanaf or periode_1_tm < periode_2_vanaf: return False else: return True def main(): print('Overlappen twee perioden elkaar?') periode_1_vanaf = input('Eerste periode, vanaf-datum (JJJJMMDD) ') periode_1_tm = input('Eerste periode, t/m-datum (JJJJMMDD) ' ) periode_2_vanaf = input('Tweede periode, vanaf-datum (JJJJMMDD) ') periode_2_tm = input('Tweede periode, t/m-datum (JJJJMMDD) ') print(' ') print('Eerste periode: ', periode_1_vanaf, 't/m', periode_1_tm) print('Tweede periode: ', periode_2_vanaf, 't/m', periode_2_tm) if check_perioden_overlappen_elkaar( periode_1_vanaf, periode_1_tm, periode_2_vanaf, periode_2_tm): print('De perioden overlappen elkaar') else: print('De perioden overlappen elkaar niet.') if __name__ == '__main__': main()
Met 'situaties met zij-effecten' bedoelen we
condities die waarden toekennen aan variabelen,
foutsituaties genereren, iets afdrukken,
iets naar een file wegschrijven, e.d.
Als ik een bestand ga inlezen op de volgende manier
def lees_bestand(): file_name = 'een_csv_bestand.csv' file_object = open(file_name) line = file_object.readline() print(line) while line != '': line = file_object.readline() print(line) file_object.close() lees_bestand()
dan lees ik regel voor regel in, en gebeuren er iedere keer als ik een volgende regel inlees twee dingen:
David;20240112;35.00 Jim;20240123;44.00 Ken;20240125;32.81 John;20240131;4.33 David;20240204;35.00 John;20240205;21.61 Ken;20240221;12.32 David;20240228;16.37
De output wordt dan
DDavid;20240112;35.00 Jim;20240123;44.00 Ken;20240125;32.81 John;20240131;4.33 David;20240204;35.00 John;20240205;21.61 Ken;20240221;12.32 David;20240228;16.37
Veronderstel nu dat er een verzoek ligt om de lege regels uit het overzicht te halen. Voor dat verzoek heb je geen last van het zij-effect. Je kunt de onzichtbare tekens \r en \n (CR en LF) uit de print-statements halen.
def lees_bestand(): file_name = 'een_csv_bestand.csv' file_object = open(file_name) line = file_object.readline() print(line, end='') while line != '': line = file_object.readline() print(line, end='') file_object.close() lees_bestand()
De output wordt dan:
David;20240112;35.00 Jim;20240123;44.00 Ken;20240125;32.81 John;20240131;4.33 David;20240204;35.00 John;20240205;21.61 Ken;20240221;12.32 David;20240228;16.37
Je kunt ook de tekens \r en \n (CR en LF) uit de variabele line zelf halen.
def lees_bestand(): file_name = 'een_csv_bestand.csv' file_object = open(file_name) line = file_object.readline().rstrip() print(line) while line != '': line = file_object.readline().rstrip() print(line) file_object.close() lees_bestand()
Laten we nu aannemen dat het input-bestand bedragen voorstelt die David, Jim, Ken en John hebben betaald op verschillende dagen. We willen nu de totaalbedragen die David, Jim, Ken en John hebben betaald berekenen. Nu krijgen we wel 'last' van het zij-effect dat gegevens verdwijnen bij elke inlees-actie. We moeten nu een stuk geheugen reserveren om de gegevens te bewaren, die anders zouden verdwijnen. Dit chache-geheugen kan allerlei vormen krijgen. In het volgende voorbeeld wordt een dictionary als cache-geheugen gebruikt, waarin de tussentotalen per persoon worden bijgehouden.
import decimal def lees_bestand(): cache = {} # ➊ file_name = 'een_csv_bestand.csv' file_object = open(file_name) line = file_object.readline().rstrip() while line != '': fields = line.split(';') name = fields[0] # ➋ amount = decimal.Decimal(fields[2]) # ➌ if name in cache: # ➍ cache[name] += amount # ➎ else: # ➏ cache[name] = amount # ➐ line = file_object.readline().rstrip() file_object.close() print( cache.items() ) # ➑ lees_bestand()
Bij ➊ definieer je cache-geheugen als dicionary. Bij ➋ geef je het eerste item in een regel de naam name. Bij ➌ zet je het derde item in een regel om naar een decimaal getal dat je amount noemt. ➍ Als het eerste item van de regel al in cache voorkomt ➎ dan tel je amount op bij het bedrag dat daar al staat, ➏ anders ➐ maak je een nieuw item aan in cache. Bij ➑ laat je de inhoud van de dictionary zien. De output wordt:
De dictionary kun je iets netter afdrukken:
import decimal from pprint import pprint def lees_bestand(): cache = {} file_name = 'een_csv_bestand.csv' file_object = open(file_name) line = file_object.readline().rstrip() while line != '': fields = line.split(';') name = fields[0] amount = decimal.Decimal(fields[2]) if name in cache: cache[name] += amount else: cache[name] = amount line = file_object.readline().rstrip() file_object.close() pprint(cache) lees_bestand()
geeft output
{'David': Decimal('86.37'), 'Jim': Decimal('44.00'), 'John': Decimal('25.94'), 'Ken': Decimal('45.13')}
Als verschillende functies heel erg op elkaar lijken, kun je ze vaak samenvoegen met het strategy pattern. Vergelijk in het volgende programma de methoden kleinste() en som().
def kleinste(lijst): kleinste_tot_nu_toe = lijst[0] i = 0 while i < len(lijst): if kleinste_tot_nu_toe > lijst[i]: kleinste_tot_nu_toe = lijst[i] i += 1 return kleinste_tot_nu_toe def som(lijst): som_tot_nu_toe = 0 i = 0 while i < len(lijst): som_tot_nu_toe += lijst[i] i += 1 return som_tot_nu_toe if __name__ == '__main__': lijst = [5, 7, 9, 3, 11, 13, 15] print( kleinste(lijst) ) # 3 print( som(lijst) ) # 63
Als je het while-statement vervangt door een for-statement, hoef je de hulp-variabele i niet meer te gebruiken. Voorts veranderen we de variabelen kleinste_tot_nu_toe en som_tot_nu_toe. We noemen ze beide tussenstand.
def kleinste(lijst): tussenstand = lijst[0] for item in lijst: if tussenstand > item: tussenstand = item return tussenstand def som(lijst): tussenstand = 0 for item in lijst: tussenstand += item return tussenstand if __name__ == '__main__': lijst = [5, 7, 9, 3, 11, 13, 15] print( kleinste(lijst) ) # 3 print( som(lijst) ) # 63
In bovenstaande code zijn twee gedeeltes vetgedrukt. Deze delen gaan we opnemen in een callable class.
class ProcessKleinste: def __init__(self, item): self.item = item self.tussenstand = lijst[0] def __call__(self): if self.tussenstand > self.item: self.tussenstand = self.item
Voorafgaand aan het for-statement maken we een object van class ProcessKleinste. Dit object noemen we process_kleinste. Binnen de for-loop roepen we process_item aan, waardoor we de methode __call__() uitvoeren.
def kleinste(lijst): process_kleinste = ProcessKleinste() for item in lijst: eindstand = process_kleinste(item) return eindstand class ProcessKleinste: def __init__(self): self.tussenstand = lijst[0] def __call__(self, item): if self.tussenstand > item: self.tussenstand = item return self.tussenstand def som(lijst): tussenstand = 0 for item in lijst: tussenstand += item return tussenstand if __name__ == '__main__': lijst = [5, 7, 9, 3, 11, 13, 15] print( kleinste(lijst) ) print( som(lijst) )
Iets soortgelijks doen we bij de functie som().
def kleinste(lijst): process_kleinste = ProcessKleinste() for item in lijst: eindstand = process_kleinste(item) return eindstand class ProcessKleinste: def __init__(self): self.tussenstand = lijst[0] def __call__(self, item): if self.tussenstand > item: self.tussenstand = item return self.tussenstand def som(lijst): process_som = ProcessSom() for item in lijst: eindstand = process_som(item) return eindstand class ProcessSom: def __init__(self): self.tussenstand = 0 def __call__(self, item): self.tussenstand += item return self.tussenstand if __name__ == '__main__': lijst = [5, 7, 9, 3, 11, 13, 15] print( kleinste(lijst) ) # 3 print( som(lijst) ) # 63
Je zou de lijst ook éénmalig kunnen doorlopen:
class ProcessKleinste: def __init__(self): self.tussenstand = lijst[0] def __call__(self, item): if self.tussenstand > item: self.tussenstand = item return self.tussenstand class ProcessSom: def __init__(self): self.tussenstand = 0 def __call__(self, item): self.tussenstand += item return self.tussenstand def process_lijst(lijst): process_kleinste = ProcessKleinste() process_som = ProcessSom() for item in lijst: eindstand_kleinste = process_kleinste(item) eindstand_som = process_som(item) return eindstand_kleinste, eindstand_som if __name__ == '__main__': lijst = [5, 7, 9, 3, 11, 13, 15] print( process_lijst(lijst) ) # (3, 63)
Je kunt de verschillende code-fragmenten ook als volgt combineren:
class ProcessKleinste: def __init__(self): self.tussenstand = lijst[0] def __call__(self, item): if self.tussenstand > item: self.tussenstand = item return self.tussenstand class ProcessSom: def __init__(self): self.tussenstand = 0 def __call__(self, item): self.tussenstand += item return self.tussenstand def process_batch(lijst, Processor): process = Processor() for item in lijst: eindstand = process(item) return eindstand if __name__ == '__main__': lijst = [5, 7, 9, 3, 11, 13, 15] print( process_batch(lijst, ProcessKleinste) ) # 3 print( process_batch(lijst, ProcessSom) ) # 63
Het kan nooit kwaad om na te denken over een betere naamgeving. Het is usance om vooral engelse benamingen te gebruiken. Dan krijg je bijvoorbeeld:
class MinimumProcessor: def __init__(self): self.intermediate_value = batch[0] def __call__(self, item): if self.intermediate_value > item: self.intermediate_value = item return self.intermediate_value class SumProcessor: def __init__(self): self.intermediate_value = 0 def __call__(self, item): self.intermediate_value += item return self.intermediate_value def process_batch(batch, Processor): process = Processor() for item in batch: final_value = process(item) return final_value if __name__ == '__main__': batch = [5, 7, 9, 3, 11, 13, 15] print( process_batch(batch, MinimumProcessor) ) print( process_batch(batch, SumProcessor) )
De 'law of Demeter' zoals die op internet wordt uitgelegd is niet altijd toepasbaar als je gebruik maakt van de computertaal Python. Je vindt op internet beschrijvingen als: De 'Law of Demeter' vereist dat een methode meth van object objct alleen methoden mag aanroepen van de volgende soorten objecten:
class Baas(): def __init__(self, naam): self.naam = naam def commandeer_hond_rechter_voorpoot_te_bewegen(self, hond): hond._beweeg_rechter_voorpoot() class Hond(): def __init__(self, naam): self.naam = naam def _beweeg_rechter_voorpoot(self): print('de hond beweegt rechter voorpoot') def _beweeg_linker_voorpoot(self): print('de hond beweegt linker voorpoot') def _beweeg_rechter_achterpoot(self): print('de hond beweegt rechter achterpoot') def _beweeg_linker_achterpoot(self): print('de hond beweegt rechter achterpoot') def lopen(self): self._beweeg_rechter_voorpoot() self._beweeg_linker_achterpoot() self._beweeg_linker_voorpoot() self._beweeg_rechter_achterpoot() print('de hond loopt') def main(): jan = Baas('Jan') fik = Hond('Fik') jan.commandeer_hond_rechter_voorpoot_te_bewegen(fik) if __name__ == '__main__': main()
De output is:
de hond beweegt rechter voorpoot
Het programma lijkt goed te werken. Maar in de regel 'hond._beweeg_rechter_voorpoot()' wordt een methode aangeroepen, die met een underscore begint. Met zo'n underscore aan het begin van een methode geeft een programmeur aan dat het niet de bedoeling is, dat de methode niet door een object buiten buiten het Hond-object wordt aangeroepen. Om dat het object jan is geen Hond-object is, schendt het programma de 'law of Demeter'. jan mag geen methode aanroepen die bedoeld is voor intern gebruik binnen de class Hond.
In programma's waarin hiërarchieën van objecten worden gedefinieerd,
of anderszins afstanden tussen objecten bestaan,
dan is het verstandig de 'law of Demeter' in acht te nemen.
De 'law of Demeter' wordt ook wel geformuleerd als 'spreek niet met vreemden' of
'spreek alleen met je buren'.
Je moet de 'law of Demeter' in gedachten houden, maar niet beschouwen als een dogma.
In de programmeertaal Java is het gebruik van getters en setter standaard ingebouwd in de taal. In Python hebben getters en setters eigenlijk geen zin. Een variabele die gedefinieerd wordt binnen een class, kan voor zover de regels van de namespaces het toelaten, op de gewone manier gewijzigd worden.
class GetSetX: def __init__(self, x): self.x = x def main(): obj = GetSetX('obj') print( obj.x ) # obj obj.x = 'obj_new' print( obj.x ) # obj_new if __name__ == '__main__': main()
In Java worden speciale methodes toegevoegd aan de classes, waarmee variabelen in objecten kunnen worden opgevraagd en gewijzigd.
class GetSetX: def __init__(self, x): self.x = x def get_x(self): return self.x def set_x(self, y): self.x = y return self.x def main(): obj = GetSetX('obj') print( obj.get_x() ) # obj obj.set_x('obj_new') print( obj.get_x() ) # obj_new if __name__ == '__main__': main()
Als je de laatste drie regels van main() verplaatst naar een method van een nieuwe class U, moet je het object obj global maken, anders kan obj niet benaderd worden vanuit class U.
class GetSetX: def __init__(self, x): self.x = x def get_x(self): return self.x def set_x(self, y): self.x = y return self.x class U: def __init__(self, u_name): self.u_name = u_name def change_x(self): print( obj.get_x() ) # obj obj.set_x('obj_new') print( obj.get_x() ) # obj_new def main(): global obj obj = GetSetX('obj') u = U('u_name') u.change_x() if __name__ == '__main__': main()
Een globale variabele kun je overal vandaan wijzigen. Als je dat op meerdere plaatsen in je programma doet, is de kans groot dat je op een gegeven moment geen idee meer hebt door welk commando de variabele werd gewijzigd. Het wijzigen van globale variabelen is vragen om moeilijkheden. De regel 'Do not use getters or setters' zou je in Python moeten vervangen door 'Gebruik geen variabelen of methoden die enkel voor intern gebruik binnen de objecten zelf bedoeld zijn'.